/*
 * Copyright 2009-2010 Belmont Software Services
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.belmont.backup;

import java.util.*;
import java.util.zip.*;
import java.net.*;
import java.io.*;
import org.json.*;
import com.marimba.intf.application.IApplicationContext;
import com.marimba.intf.util.IConfig;
import com.marimba.intf.http.HTTPConstants;
import com.marimba.intf.http.IConnection;
import com.marimba.intf.plugin.IHttpRequest;

public class BackupServerClient implements IBackupServer, IBackupConstants, Runnable {
    static final boolean ENABLE_KEEP_ALIVE = false;
    HttpURLConnection conn;
    String baseURL;
    IApplicationContext context;
    IConfig channelConfig;
    Config config = new Config();
    String defaultSession;
    String defaultUser;
    String defaultUUID;
    File root;
    File tmpRoot;
    long lastConfigCheck;
    Thread configCheckThread;

    public BackupServerClient(String baseURL, IApplicationContext context) {
	this.baseURL = baseURL;
	this.context = context;
	channelConfig = context.getConfiguration();

	try {
	    lastConfigCheck = Long.parseLong(channelConfig.getProperty(CONFIG_CONFIG_SCHEDULE_CHECKED));
	} catch (NumberFormatException ex) {
	    Utils.log(LOG_ERROR, "Format error in config schedule checked property", ex);
	}

	initCompressedTypes();
    }

    public BackupServerClient(File root, String host, int port) {
	this.baseURL = "http://"+host+":"+port;
	this.root = root;
	this.tmpRoot = new File(root, "tmp");
	tmpRoot.mkdirs();
	initCompressedTypes();
    }

    public void run() {
        try {
            while (true) {
                Thread.sleep(20000);
                getConfig();
            }
        } catch (InterruptedException ex) {
            // just exit if we get interrupted
        }
    }

    public HttpURLConnection prepareRequest(String session, String url,String method,
					    String type, long length, InputStream dataStream,
					    Hashtable<String, String> properties) throws IOException, MalformedURLException {
        URL u = new URL(url);
        HttpURLConnection conn;

        conn = (HttpURLConnection)(u.openConnection());

        conn.setRequestMethod(method);
        conn.setRequestProperty(REQUEST_PROP_USER_AGENT, "com.belmont.backup.BackupClient");
        conn.setRequestProperty(REQUEST_PROP_HOST, Utils.getHostName());
        conn.setRequestProperty(REQUEST_PROP_HOSTIP, Utils.getHostIP());
        conn.setRequestProperty(REQUEST_PROP_OS, System.getProperty("os.name"));
        conn.setRequestProperty(REQUEST_PROP_OSVERSION, System.getProperty("os.version"));
        conn.setRequestProperty(REQUEST_PROP_PROTOCOL, BACKUP_PROTOCOL_VERSION);
        if (defaultUser != null) {
            conn.setRequestProperty(REQUEST_PROP_USER, defaultUser);
        }
        if (defaultUUID != null) {
            conn.setRequestProperty(REQUEST_PROP_UUID, defaultUUID);
        }
        if (! ENABLE_KEEP_ALIVE) {
            conn.setRequestProperty(REQUEST_PROP_CONNECTION, "Close");
        }
        if (session != null) {
            conn.setRequestProperty(REQUEST_PROP_SESSION, session);
        }
        if (properties != null) {
            Enumeration<String> k = properties.keys();
            while (k.hasMoreElements()) {
                String key = k.nextElement();
                conn.setRequestProperty(key, properties.get(key));
            }
        }

        conn.setDoInput(true);
        conn.setDoOutput("POST".equals(method));

        if (dataStream != null) {
            conn.setRequestProperty(REQUEST_PROP_CONTENT_LENGTH, Long.toString(length));
            conn.setRequestProperty(REQUEST_PROP_CONTENT_TYPE, type);
        } else {
            conn.setRequestProperty(REQUEST_PROP_CONTENT_LENGTH, "0");
        }

        conn.connect();

        return conn;
    }

    public synchronized HttpURLConnection startRequest(String session,
					  String url,
					  String method,
					  String type,
					  long length,
					  InputStream dataStream,
					  Hashtable<String, String> properties) throws IOException, MalformedURLException {
        HttpURLConnection conn = prepareRequest(session, url, method, type, length, dataStream, properties);
        OutputStream out = null;

        if (dataStream != null) {
            out = conn.getOutputStream();

            byte buf[] = BufferPool.getInstance().get(1024);
            int len;
            int wlen = 0;

            try {
                while ((len = dataStream.read(buf)) != -1) {
                    out.write(buf, 0, len);
                    length -= len;
                    wlen += len;
                    if (length <= 0) {
                        break;
                    }
                }

                BufferPool.getInstance().put(buf);
            } finally {
                dataStream.close();
            }
        }
        int cresp = conn.getResponseCode();
        if (cresp == 200) {
            return conn;
        } else {
            if (! ENABLE_KEEP_ALIVE) {
                if (out != null) {
                    out.close();
                }
            }
            return null;
        }
    }

    public String sendRequest(String session,
			      String url,
			      String method,
			      String type,
			      long length,
			      InputStream dataStream) throws IOException, MalformedURLException {
	return sendRequest(session, url, method, type, length, dataStream, null);
    }

    public String sendRequest(String session,
			      String url,
			      String method,
			      String type,
			      long length,
			      InputStream dataStream,
			      Hashtable<String, String> properties) throws IOException, MalformedURLException {
	HttpURLConnection conn = startRequest(session, url, method, type, length, dataStream, properties);

	if (conn != null) {
	    InputStream in = conn.getInputStream();
	    StringBuffer sb = new StringBuffer();
	    byte buf[] = BufferPool.getInstance().get(1024);

	    int len;
	    int rlength = conn.getContentLength();
	    try {
		int rlen = Math.min(rlength, buf.length);
		while (rlength > 0 && (len = in.read(buf, 0, rlen)) != -1) {
		    sb.append(new String(buf, 0, len));
		    rlength -= len;
		    rlen = Math.min(rlength, buf.length);
		}
	    } finally {
		BufferPool.getInstance().put(buf);
	    }
	    return sb.toString();
	} else {
	    throw new IOException("ERROR: connection error.");
	}
    }

    /**
     * Initiate a session with the server. user can be any string
     * that describes the user of the server. For network sessions,
     * authentication is done using http digest authentication. The
     * returned value is a session string that should be passed on
     * to all subsequent requests to the server.
     */
    public synchronized String connect(String protocolVersion, String uuid, String host,
				       String osname, String osversion, String user) throws IOException {
	defaultUser = user;
	defaultUUID = uuid;
	defaultSession = sendRequest(null, getBackupServerURL(REQUEST_CONNECT),"GET", null, 0, null);
	configCheckThread = new Thread(this);
	configCheckThread.start();
	return defaultSession;
    }

    public synchronized Config getConfig() {
	long now = System.currentTimeMillis();

	if (config.getConfigSchedule() == null) {
	    if (lastConfigCheck == 0 ||
		((now - lastConfigCheck) > 10000)) {
		try {
		    refresh();
		    lastConfigCheck = now;
		    if (channelConfig != null) {
			channelConfig.setProperty(CONFIG_CONFIG_SCHEDULE_CHECKED, Long.toString(lastConfigCheck));
		    }
		} catch (IOException ex) {
		    Utils.log(LOG_ERROR, "Error getting config: ",ex);
		}
	    }
	} else {
	    int h = 1;

	    try {
		h = Integer.parseInt(config.getConfigSchedule());
	    } catch (NumberFormatException ex) {
		Utils.log(LOG_ERROR, "Bad number format for config schedule", ex);
	    }

	    if ((lastConfigCheck == 0) ||
		((now - lastConfigCheck) > ((long)(h * 60000)))) {
		try {
		    refresh();
		    lastConfigCheck = now;
		    if (channelConfig != null) {
			channelConfig.setProperty(CONFIG_CONFIG_SCHEDULE_CHECKED, Long.toString(lastConfigCheck));
		    }
		} catch (IOException ex) {
		    Utils.log(LOG_ERROR, "Error getting config: ",ex);
		}
	    }
	}

		
	return config;
    }

    /**
     * Refreshes any configuration options from the server.
     */
    public synchronized void refresh() throws IOException {
	String configString = sendRequest(defaultSession, getBackupServerURL(REQUEST_GETCONFIG),"GET", null, 0, null);
	if (configString != null) {
	    try {
		config.initFrom(new JSONObject(configString));
	    } catch (JSONException ex) {
		throw new IOException("JSON format error in config string: "+configString);
	    }
	}
    }

    public String getBaseURL() {
	return baseURL;
    }

    String getBackupServerURL(String request) {
	return getBackupServerURL(request, false);
    }

    String getBackupServerURL(String request, boolean post) {
	String sep = (baseURL.endsWith("/") ? "" : "/");
	//return baseURL+sep+"backup/"+request+((context != null && !post) ? "?plugin" : "");
	return baseURL+sep+"backup/"+request;
    }

    /**
     * Terminates all activity for this session.
     */
    public synchronized void disconnect(String session) throws IOException {
	if (configCheckThread != null) {
	    configCheckThread.interrupt();
	    configCheckThread = null;
	}
	sendRequest(session, getBackupServerURL(REQUEST_DISCONNECT),"GET", null, 0, null);
    }

    /**
     * Returns the available versions for this session. The keys are
     * the paths available and the value is a JSONArray of JSONObject
     * objects with keys name and date.
     */
    public synchronized JSONObject getAvailableVersions(String session) throws IOException, JSONException {
	String r = sendRequest(session, getBackupServerURL(REQUEST_GETVERSIONS),"GET", null, 0, null);
	if (r == null) {
	    return null;
	}
	return new JSONObject(r);
    }

    public IConnection preparePluginRequest(String session,
					    String url,
					    String type,
					    long length,
					    InputStream dataStream,
					    Hashtable<String, String> properties) throws IOException, MalformedURLException {
	IConnection conn = context.getPluginConnection();

	conn.open();

	conn.addField(REQUEST_PROP_URL, url);
	conn.addField(REQUEST_PROP_HOST, Utils.getHostName());
	conn.addField(REQUEST_PROP_HOSTIP, Utils.getHostIP());
	if (session != null) {
	    conn.addField(REQUEST_PROP_SESSION, session);
	}
	if (properties != null) {
	    Enumeration<String> k = properties.keys();
	    while (k.hasMoreElements()) {
		String key = k.nextElement();
		conn.addField(key, properties.get(key));
	    }
	}
	if (dataStream != null) {
	    conn.addField(REQUEST_PROP_CONTENT_LENGTH, Long.toString(length));
	    conn.addField(REQUEST_PROP_CONTENT_TYPE, type);
	} else {
	    conn.addField(REQUEST_PROP_CONTENT_LENGTH, "0");
	}

	return conn;
    }

    public String sendPluginRequest(String session,
				    String url,
				    String type,
				    long length,
				    InputStream dataStream,
				    Hashtable<String, String> properties) throws IOException, MalformedURLException {
	IConnection conn = preparePluginRequest(session, url, type, length, dataStream, properties);

	if (dataStream != null) {
	    OutputStream out = conn.getOutputStream((int)length);

	    byte buf[] = BufferPool.getInstance().get(1024);
	    int len;

	    try {
		while ((len = dataStream.read(buf)) != -1) {
		    out.write(buf, 0, len);
		    length -= len;
		    if (length <= 0) {
			break;
		    }
		}
	    } finally {
		BufferPool.getInstance().put(buf);
		dataStream.close();
	    }
	}

	if (conn.reply() == 200) {
	    InputStream in = conn.getInputStream();
	    StringBuffer sb = new StringBuffer();
	    int len;
	    byte buf[] = BufferPool.getInstance().get(1024);

	    try {
		while ((len = in.read(buf)) != -1) {
		    sb.append(new String(buf, 0, len));
		}
	    } finally {
		BufferPool.getInstance().put(buf);
		if (! ENABLE_KEEP_ALIVE) {
		    in.close();
		}
	    }
	    return sb.toString();
	} else {
	    throw new IOException("Error in sendPluginRequest");
	}
    }

    public synchronized JSONObject startBackup(String session, String path) throws IOException, JSONException {
	Hashtable<String, String> properties = new Hashtable<String, String>();

	properties.put(REQUEST_PROP_BACKUP_PATH, path);
	String response = sendRequest(session, getBackupServerURL(REQUEST_STARTBACKUP), "POST", "application/zip", 0, null, properties);
	if (response != null) {
	    JSONObject obj = new JSONObject(response);
	    return obj;
	}

	return null;
    }

    public synchronized JSONObject checkFiles(String session, String backupid, String path, JSONObject fileRequest) throws IOException, JSONException {
	if (fileRequest == null) {
	    return null;
	}

	Hashtable<String, String> properties = new Hashtable<String, String>();
	properties.put(REQUEST_PROP_BACKUP_PATH, path);
	properties.put(REQUEST_PROP_BACKUP_ID, backupid);

	String fstring = fileRequest.toString();
	StringBufferInputStream in = new StringBufferInputStream(fstring);

	try {
	    String response = sendRequest(session, getBackupServerURL(REQUEST_CHECKFILES), "POST", "application/json", fstring.length(), in, properties);
	    if (response != null) {
		JSONObject obj = new JSONObject(response);
		return obj;
	    }
	} finally {
	    in.close();
	}
					  
	return null;
    }

    Hashtable<String, String> compressedTypes = new Hashtable<String, String>();

    void initCompressedTypes() {
	compressedTypes.put("zip", "zip");
	compressedTypes.put("jpg", "jpg");
	compressedTypes.put("jpeg", "jpeg");
	compressedTypes.put("mp4", "mp4");
	compressedTypes.put("mp3", "mp3");
	compressedTypes.put("wav", "wav");
	compressedTypes.put("aac", "aac");
	compressedTypes.put("gzip", "gzip");
    }

    boolean uncompressedSuffix(String name) {
	int idx = name.lastIndexOf('.');
	if (idx == -1) {
	    return true;
	}
	name = name.substring(idx+1).toLowerCase();
	return compressedTypes.get(name) == null;
    }

    void addToCompressedTypes(String name) {
	int idx = name.lastIndexOf('.');
	if (idx == -1) {
	    return;
	}
	name = name.substring(idx+1).toLowerCase();
	compressedTypes.put(name, name);
    }

    /**
     * Sends a file to the server with the information about the file
     * contained in req.
     */
    public synchronized void sendFile(String session, String digest,
				      String name, long length, long offset,
				      InputStream in) throws IOException, InterruptedException {

        Hashtable<String, String> properties = new Hashtable<String, String>();

        properties.put(REQUEST_PROP_BACKUP_FILE_DIGEST, digest);
        properties.put(REQUEST_PROP_BACKUP_FILE_NAME, name);
        properties.put(REQUEST_PROP_BACKUP_FILE_LENGTH, Long.toString(length));
        properties.put(REQUEST_PROP_BACKUP_FILE_OFFSET, Long.toString(offset));
        boolean canCompress = uncompressedSuffix(name);
        File compressedFile = null;

        if (canCompress) {
            compressedFile = File.createTempFile("backup", null, tmpRoot);
            DeflaterOutputStream dout = new DeflaterOutputStream(new FileOutputStream(compressedFile));
            byte buf[] = BufferPool.getInstance().get(1024);
            int len;
            int clength = 0;
            try {
                while ((len = in.read(buf)) != -1) {
                    dout.write(buf, 0, len);
                    Utils.checkCancel();
                    clength += len;
                }
            } finally {
                BufferPool.getInstance().put(buf);
                dout.close();
            }

            if (clength >= length) {
                // XXX: if clength is bigger than length we need to
                // adjust the length parameter in the http header

                // if compression didn't shrink the file remember the
                // file suffix to avoid doing compression in the
                // future. 
                addToCompressedTypes(name);
            }
		
            in = new FileInputStream(compressedFile);
            properties.put(REQUEST_PROP_BACKUP_FILE_COMPRESSED_LENGTH, Integer.toString(clength));
        }

        HttpURLConnection conn = prepareRequest(session, getBackupServerURL(REQUEST_SENDFILE),
                                                "POST",
                                                "application/zip",
                                                length, in, properties);

        int len;
        int sent = 0;
        byte buf[] = BufferPool.getInstance().get(4096);

        OutputStream out = conn.getOutputStream();
        Status status = Status.getInstance();
        try {
            while ((len = in.read(buf)) != -1) {
                Utils.checkCancel();
                out.write(buf, 0, len);
                sent += len;
                status.addAmountTransferred((long)len);
            }
            if (compressedFile != null) {
                if (sent != compressedFile.length()) {
                    throw new IOException("Error sending compressed file length mismatch "+sent+" != "+compressedFile.length());
                }
            } else if (sent != length) {
                throw new IOException("Error sending file length mismatch "+sent+" != "+length);
            }
        } finally {
            BufferPool.getInstance().put(buf);
            if (compressedFile != null) {
                compressedFile.delete();
            }
        }

        try {
            int c = conn.getResponseCode();
            switch (c) {
                case 200:
                    break;
                default:
                    throw new IOException("Received error sending file "+c);
            }
        } finally {
            if (! ENABLE_KEEP_ALIVE) {
                out.close();
            }
        }
    }
    

    /**
     * Gets a file identified by req from the server.
     */
    public synchronized void getFile(String session,
				     String digest,
				     String name,
				     long length,
				     long offset,
				     OutputStream out) throws IOException, InterruptedException {
	Hashtable<String, String> properties = new Hashtable<String, String>();

	properties.put(REQUEST_PROP_BACKUP_FILE_DIGEST, digest);
	properties.put(REQUEST_PROP_BACKUP_FILE_NAME, name);
	properties.put(REQUEST_PROP_BACKUP_FILE_LENGTH, Long.toString(length));
	properties.put(REQUEST_PROP_BACKUP_FILE_OFFSET, Long.toString(offset));
	    
	HttpURLConnection conn = startRequest(session, getBackupServerURL(REQUEST_GETFILE),
					      "GET","application/zip",
					      length, null, properties);

	InputStream in = conn.getInputStream();
	byte buf[] = BufferPool.getInstance().get(1024);
	int len;
	int rlength = conn.getContentLength();
	int rlen = Math.min(rlength, buf.length);

	try {
	    Status status = Status.getInstance();
	    while (rlength > 0 && (len = in.read(buf, 0, rlen)) != -1) {
		out.write(buf, 0, len);
		Utils.checkCancel();
		status.addAmountTransferred((long)len);
		rlength -= len;
		rlen = Math.min(rlength, buf.length);
	    }
	} finally {
	    BufferPool.getInstance().put(buf);
	    if (! ENABLE_KEEP_ALIVE) {
		in.close();
	    }
	}
    }


    /**
     * Cancels an ongoing backup operation.
     */
    public synchronized void cancelBackup(String session, String path) throws IOException {
	Hashtable<String, String> properties = new Hashtable<String, String>();
	properties.put(REQUEST_PROP_BACKUP_PATH, path);

	sendRequest(session, getBackupServerURL(REQUEST_CANCELBACKUP),"GET", null, 0, null, properties);
    }

    public synchronized void commitBackup(String session, String backupid, String path) throws IOException {
	Hashtable<String, String> properties = new Hashtable<String, String>();
	properties.put(REQUEST_PROP_BACKUP_PATH, path);
	if (backupid != null) {
	    properties.put(REQUEST_PROP_BACKUP_ID, backupid);
	}
	sendRequest(session, getBackupServerURL(REQUEST_COMMITBACKUP),"GET", null, 0, null, properties);
    }

    /**
     * Commmits an ongoing backup operation for this session and path.
     */
    public synchronized void commitBackup(String session, String path) throws IOException {
	commitBackup(session, null, path);
    }
}